using System.Collections;
using System.Collections.Generic;

namespace LightCAD.Three
{
    public class Box3
    {
        #region scope properties or methods
        //private static Vector3[] _points = new Vector3[]
        //{
        //    new Vector3(),
        //    new Vector3(),
        //    new Vector3(),
        //    new Vector3(),
        //    new Vector3(),
        //    new Vector3(),
        //    new Vector3(),
        //    new Vector3()
        //};
        //private static Vector3 _vector = new Vector3();
        //private static Box3 _box = new Box3();
        //private static Vector3 _v0 = new Vector3();
        //private static Vector3 _v1 = new Vector3();
        //private static Vector3 _v2 = new Vector3();
        //private static Vector3 _f0 = new Vector3();
        //private static Vector3 _f1 = new Vector3();
        //private static Vector3 _f2 = new Vector3();
        //private static Vector3 _center = new Vector3();
        //private static Vector3 _extents = new Vector3();
        //private static Vector3 _triangleNormal = new Vector3();
        //private static Vector3 _testAxis = new Vector3();
        private static Box3Context getContext()
        {
            return ThreadContext.getCurrThreadContext().Box3Ctx;
        }
        private static bool satForAxes(double[] axes, Vector3 v0, Vector3 v1, Vector3 v2, Vector3 extents)
        {
            var _testAxis = getContext()._testAxis;
            for (int i = 0, j = axes.Length - 3; i <= j; i += 3)
            {

                _testAxis.fromArray(axes, i);
                // project the aabb onto the separating axis

                // project all 3 vertices of the triangle onto the separating axis
                double r = extents.x * JMath.abs(_testAxis.x) + extents.y * JMath.abs(_testAxis.y) + extents.z * JMath.abs(_testAxis.z);

                double p0 = v0.dot(_testAxis);
                double p1 = v1.dot(_testAxis);
                double p2 = v2.dot(_testAxis);


                // actual test, basically see if either of the most extreme of the triangle points intersects r
                if (JMath.max(-JMath.max(p0, p1, p2), JMath.min(p0, p1, p2)) > r)
                {

                    // points of the projected triangle are outside the projected half-length of the aabb
                    // the axis is separating and we can exit
                    return false;

                }

            }

            return true;

        }
        #endregion

        #region Properties



        public Vector3 min;
        public Vector3 max;

        #endregion

        #region constructor
        public Box3(Vector3 min = null, Vector3 max = null)
        {
            if (min == null) min = new Vector3(double.PositiveInfinity, double.PositiveInfinity, double.PositiveInfinity);
            if (max == null) max = new Vector3(double.NegativeInfinity, double.NegativeInfinity, double.NegativeInfinity);

            this.min = min;
            this.max = max;
        }
        #endregion

        #region methods
        public Box3 set(Vector3 min, Vector3 max)
        {
            this.min.copy(min);
            this.max.copy(max);
            return this;
        }
        public Box3 setFromArray(double[] array)
        {
            var _vector = getContext()._vector;
            this.makeEmpty();

            for (int i = 0, il = array.Length; i < il; i += 3)
            {
                this.expandByPoint(_vector.fromArray(array, i));
            }
            return this;

        }
        public Box3 setFromBufferAttribute(BufferAttribute attribute)
        {
            var _vector = getContext()._vector;
            this.makeEmpty();
            for (int i = 0, il = attribute.count; i < il; i++)
            {
                this.expandByPoint(_vector.fromBufferAttribute(attribute, i));
            }

            return this;
        }
        public Box3 setFromPoints(IList<Vector3> points)
        {
            this.makeEmpty();
            for (int i = 0, il = points.Count; i < il; i++)
            {
                this.expandByPoint(points[i]);
            }
            return this;
        }
        public Box3 setFromCenterAndSize(Vector3 center, Vector3 size)
        {
            var _vector = getContext()._vector;
            Vector3 halfSize = _vector.copy(size).multiplyScalar(0.5);
            this.min.copy(center).sub(halfSize);
            this.max.copy(center).add(halfSize);
            return this;
        }
        public Box3 setFromObject(Object3D _object, bool precise = false)
        {
            this.makeEmpty();
            return this.expandByObject(_object, precise);
        }
        public Box3 clone()
        {
            return new Box3().copy(this);
        }
        public Box3 copy(Box3 box)
        {
            this.min.copy(box.min);
            this.max.copy(box.max);
            return this;
        }
        public Box3 makeEmpty()
        {
            this.min.x = this.min.y = this.min.z = double.PositiveInfinity;
            this.max.x = this.max.y = this.max.z = double.NegativeInfinity;
            return this;
        }
        public bool isEmpty()
        {
            // this is a more robust check for empty than ( volume <= 0 ) because volume can get positive with two negative axes
            return (this.max.x < this.min.x) || (this.max.y < this.min.y) || (this.max.z < this.min.z);
        }
        public Vector3 getCenter(Vector3 target = null)
        {
            target = target ?? new Vector3();
            return this.isEmpty() ? target.set(0, 0, 0) : target.addVectors(this.min, this.max).multiplyScalar(0.5);
        }
        public Vector3 getSize(Vector3 target = null)
        {
            target = target ?? new Vector3();
            return this.isEmpty() ? target.set(0, 0, 0) : target.subVectors(this.max, this.min);
        }
        public Box3 expandByPoint(Vector3 point)
        {
            this.min.min(point);
            this.max.max(point);
            return this;
        }
        public Box3 expandByVector(Vector3 vector)
        {
            this.min.sub(vector);
            this.max.add(vector);
            return this;
        }
        public Box3 expandByScalar(double scalar)
        {
            this.min.addScalar(-scalar);
            this.max.addScalar(scalar);
            return this;
        }
        public Box3 expandByCenter(Vector3 scale)
        {
            var size = getSize(new Vector3()).multiply(scale);
            var center = getCenter(new Vector3());
            this.min.set(center.x - size.x / 2, center.y - size.y / 2, center.z - size.z / 2);
            this.max.set(center.x + size.x / 2, center.y + size.y / 2, center.z + size.z / 2);
            return this;
        }
        public Box3 expandByObject(Object3D _object, bool precise = false)
        {
            var ctx = getContext();
            var _vector = ctx._vector;
            var _box = ctx._box;
            // Computes the world-axis-aligned bounding box of an object (including its children),
            // accounting for both the object"s, and children"s, world transforms
            _object.updateWorldMatrix(false, false);

            if (_object == null || !(_object is IGeometry))
                return this;

            if (_object.HasField("boundingBox"))
            {
                if (_object.GetField("boundingBox") == null)
                {
                    _object.InvokeMethod("computeBoundingBox");
                }

                _box.copy(_object.GetField("boundingBox") as Box3);
                _box.applyMatrix4(_object.matrixWorld);

                this.union(_box);

            }
            else
            {
                var geometry = (_object as IGeometry).getGeometry();

                if (geometry != null)
                {
                    if (precise && geometry.attributes != null && geometry.attributes["position"] != null)
                    {
                        var position = geometry.attributes["position"];
                        for (int i = 0, l = position.count; i < l; i++)
                        {
                            _vector.fromBufferAttribute(position, i).applyMatrix4(_object.matrixWorld);
                            this.expandByPoint(_vector);
                        }
                    }
                    else
                    {
                        if (geometry.boundingBox == null)
                        {
                            geometry.computeBoundingBox();
                        }
                        _box.copy(geometry.boundingBox);
                        _box.applyMatrix4(_object.matrixWorld);
                        this.union(_box);
                    }
                }

            }
            var children = _object.children;
            for (int i = 0, l = children.length; i < l; i++)
            {
                this.expandByObject(children[i], precise);
            }
            return this;
        }
        public bool containsPoint(Vector3 point)
        {
            return point.x < this.min.x || point.x > this.max.x ||
                point.y < this.min.y || point.y > this.max.y ||
                point.z < this.min.z || point.z > this.max.z ? false : true;
        }
        public bool containsBox(Box3 box)
        {
            return this.min.x <= box.min.x && box.max.x <= this.max.x &&
                this.min.y <= box.min.y && box.max.y <= this.max.y &&
                this.min.z <= box.min.z && box.max.z <= this.max.z;
        }
        public Vector3 getParameter(Vector3 point, Vector3 target)
        {
            // This can potentially have a divide by zero if the box
            // has a size dimension of 0.
            return target.set(
                (point.x - this.min.x) / (this.max.x - this.min.x),
                (point.y - this.min.y) / (this.max.y - this.min.y),
                (point.z - this.min.z) / (this.max.z - this.min.z)
            );
        }
        public bool intersectsBox(Box3 box)
        {
            // using 6 splitting planes to rule out intersections.
            return box.max.x < this.min.x || box.min.x > this.max.x ||
                box.max.y < this.min.y || box.min.y > this.max.y ||
                box.max.z < this.min.z || box.min.z > this.max.z ? false : true;
        }
        public bool intersectsSphere(Sphere sphere)
        {
            var ctx = getContext();
            var _vector = ctx._vector;
            // Find the point on the AABB closest to the sphere center.
            this.clampPoint(sphere.center, _vector);
            // If that point is inside the sphere, the AABB and sphere intersect.
            return _vector.distanceToSquared(sphere.center) <= (sphere.radius * sphere.radius);
        }
        public bool intersectsPlane(Plane plane)
        {
            // We compute the minimum and maximum dot product values. If those values
            // are on the same side (back or front) of the plane, then there is no intersection.
            double min, max;
            if (plane.normal.x > 0)
            {
                min = plane.normal.x * this.min.x;
                max = plane.normal.x * this.max.x;
            }
            else
            {
                min = plane.normal.x * this.max.x;
                max = plane.normal.x * this.min.x;
            }
            if (plane.normal.y > 0)
            {
                min += plane.normal.y * this.min.y;
                max += plane.normal.y * this.max.y;
            }
            else
            {
                min += plane.normal.y * this.max.y;
                max += plane.normal.y * this.min.y;
            }
            if (plane.normal.z > 0)
            {
                min += plane.normal.z * this.min.z;
                max += plane.normal.z * this.max.z;
            }
            else
            {
                min += plane.normal.z * this.max.z;
                max += plane.normal.z * this.min.z;
            }
            return (min <= -plane.constant && max >= -plane.constant);
        }
        public bool intersectsTriangle(Triangle triangle)
        {
            if (this.isEmpty())
            {
                return false;
            }
            var ctx = getContext();
            var _center = ctx._center;
            var _extents = ctx._extents;
            var _v0 = ctx._v0;
            var _v1 = ctx._v1;
            var _v2 = ctx._v2;
            var _f0 = ctx._f0;
            var _f1 = ctx._f1;
            var _f2 = ctx._f2;
            var _triangleNormal = ctx._triangleNormal;
            // compute box center and extents
            this.getCenter(_center);
            _extents.subVectors(this.max, _center);
            // translate triangle to aabb origin
            _v0.subVectors(triangle.a, _center);
            _v1.subVectors(triangle.b, _center);
            _v2.subVectors(triangle.c, _center);
            // compute edge vectors for triangle
            _f0.subVectors(_v1, _v0);
            _f1.subVectors(_v2, _v1);
            _f2.subVectors(_v0, _v2);
            // test against axes that are given by cross product combinations of the edges of the triangle and the edges of the aabb
            // make an axis testing of each of the 3 sides of the aabb against each of the 3 sides of the triangle = 9 axis of separation
            // axis_ij = u_i x f_j (u0, u1, u2 = face normals of aabb = x,y,z axes vectors since aabb is axis aligned)
            var axes = new double[] {
                    0, -_f0.z, _f0.y, 0, -_f1.z, _f1.y, 0, -_f2.z, _f2.y,
                    _f0.z, 0, -_f0.x, _f1.z, 0, -_f1.x, _f2.z, 0, -_f2.x,
                    -_f0.y, _f0.x, 0, -_f1.y, _f1.x, 0, -_f2.y, _f2.x, 0
                };
            if (!satForAxes(axes, _v0, _v1, _v2, _extents))
            {
                return false;
            }
            // test 3 face normals from the aabb
            axes = new double[] { 1, 0, 0, 0, 1, 0, 0, 0, 1 };
            if (!satForAxes(axes, _v0, _v1, _v2, _extents))
            {
                return false;
            }
            // finally testing the face normal of the triangle
            // use already existing triangle edge vectors here
            _triangleNormal.crossVectors(_f0, _f1);
            axes = new double[] { _triangleNormal.x, _triangleNormal.y, _triangleNormal.z };
            return satForAxes(axes, _v0, _v1, _v2, _extents);
        }
        public Vector3 clampPoint(Vector3 point, Vector3 target)
        {
            return target.copy(point).clamp(this.min, this.max);
        }
        public double distanceToPoint(Vector3 point)
        {
            var _vector = getContext()._vector;
            return this.clampPoint(point, _vector).distanceTo(point);
        }
        public Sphere getBoundingSphere(Sphere target)
        {
            if (this.isEmpty())
            {
                target.makeEmpty();
            }
            else
            {
                var _vector = getContext()._vector;
                this.getCenter(target.center);
                target.radius = this.getSize(_vector).length() * 0.5;
            }
            return target;
        }
        public Box3 intersect(Box3 box)
        {
            this.min.max(box.min);
            this.max.min(box.max);
            // ensure that if there is no overlap, the result is fully empty, not slightly empty with non-inf/+inf values that will cause subsequence intersects to erroneously return valid values.
            if (this.isEmpty()) this.makeEmpty();
            return this;
        }
        public Box3 union(Box3 box)
        {
            this.min.min(box.min);
            this.max.max(box.max);
            return this;
        }
        public Box3 applyMatrix4(Matrix4 matrix)
        {
            // transform of empty box is an empty box.
            if (this.isEmpty()) return this;
            var _points = getContext()._points;
            // NOTE: I am using a binary pattern to specify all 2^3 combinations below
            _points[0].set(this.min.x, this.min.y, this.min.z).applyMatrix4(matrix); // 000
            _points[1].set(this.min.x, this.min.y, this.max.z).applyMatrix4(matrix); // 001
            _points[2].set(this.min.x, this.max.y, this.min.z).applyMatrix4(matrix); // 010
            _points[3].set(this.min.x, this.max.y, this.max.z).applyMatrix4(matrix); // 011
            _points[4].set(this.max.x, this.min.y, this.min.z).applyMatrix4(matrix); // 100
            _points[5].set(this.max.x, this.min.y, this.max.z).applyMatrix4(matrix); // 101
            _points[6].set(this.max.x, this.max.y, this.min.z).applyMatrix4(matrix); // 110
            _points[7].set(this.max.x, this.max.y, this.max.z).applyMatrix4(matrix); // 111
            this.setFromPoints(_points);
            return this;
        }
        public Box3 translate(Vector3 offset)
        {
            this.min.add(offset);
            this.max.add(offset);
            return this;
        }
        public bool equals(Box3 box)
        {
            return box.min.equals(this.min) && box.max.equals(this.max);
        }
        #endregion

    }
}
